﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Diagnostics;

namespace DiscreteEventModel.Common {

    public class StreamBase : IDisposable {

        [StructLayout(LayoutKind.Sequential)]
        public struct Token {
            internal readonly long HeadBlockPosition;

            internal Guid HashCode {
                get {
                    using(MD5 hasher = MD5.Create()) {
                        return new Guid(BitConverter.GetBytes(this.HeadBlockPosition));
                    }
                }
            }

            internal Token(long headBlockPosition) {
                this.HeadBlockPosition = headBlockPosition;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        struct Header {
            public Guid HashCode;
            public long EndBlockPosition;
            public long Length;

            public Header(Token token, long endBlockPosition, long length) {
                this.HashCode = token.HashCode;
                this.EndBlockPosition = endBlockPosition;
                this.Length = length;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct Schema {
            public readonly static int Size = Marshal.SizeOf(typeof(Schema));
            internal int DataBlockSize;
            internal int BlockSize;
            internal long BlockCount;
            internal long AllocatedBlockCount;
            internal long HeadFreeBlockPosition;
            internal int StreamHeaderSize;
            internal int StreamCount;
        }

        private readonly MemoryMappedFile _file;
        private Schema _schema;

        public Schema GetSchema() {
            return this._schema;
        }

        public int Count {
            get {
                return this._schema.StreamCount;
            }
        }

        public StreamBase(string filePath, int dataBlockSize, int dataBlockCount) {
            this._schema.StreamHeaderSize = Marshal.SizeOf(typeof(Header));
            if(dataBlockSize < this._schema.StreamHeaderSize) {
                throw new Exception();
            }
            this._schema.StreamCount = 0;
            this._schema.DataBlockSize = dataBlockSize;
            this._schema.BlockSize = dataBlockSize + sizeof(long);
            this._schema.BlockCount = dataBlockCount;
            this._schema.AllocatedBlockCount = 0;
            this._schema.HeadFreeBlockPosition = -1;
            FileInfo fileInfo = new FileInfo(filePath);
            this._file = MemoryMappedFile.CreateFromFile(fileInfo.FullName, FileMode.Create, fileInfo.Name, Schema.Size + this._schema.BlockSize * this._schema.BlockCount, MemoryMappedFileAccess.ReadWrite);
        }

        public StreamBase(string filePath) {
            FileInfo fileInfo = new FileInfo(filePath);
            this._file = MemoryMappedFile.CreateFromFile(fileInfo.FullName, FileMode.Open, fileInfo.Name, 0, MemoryMappedFileAccess.ReadWrite);
            using(MemoryMappedViewAccessor schemaAccessor = this._file.CreateViewAccessor(0, Schema.Size)) {
                schemaAccessor.Read<Schema>(0, out this._schema);
            }
        }

        private MemoryMappedViewAccessor AllocateBlock(out long blockPosition) {
            MemoryMappedViewAccessor blockAccessor;
            blockPosition = this._schema.HeadFreeBlockPosition;
            if(blockPosition > 0) {
                blockAccessor = this._file.CreateViewAccessor(blockPosition, this._schema.BlockSize);
                this._schema.HeadFreeBlockPosition = blockAccessor.ReadInt64(this._schema.DataBlockSize);
            } else if(this._schema.AllocatedBlockCount < this._schema.BlockCount) {
                blockPosition = Schema.Size + this._schema.BlockSize * this._schema.AllocatedBlockCount++;
                blockAccessor = this._file.CreateViewAccessor(blockPosition, this._schema.BlockSize);
            } else {
                throw new Exception();
            }
            blockAccessor.Write(this._schema.DataBlockSize, -1L);
            return blockAccessor;
        }

        private bool MoveToNextBlock(ref long blockPosition, ref MemoryMappedViewAccessor blockAccessor, MemoryMappedFileAccess access, bool allocate = false) {
            if(blockAccessor == null) {
                if(blockPosition > 0) {
                    blockAccessor = this._file.CreateViewAccessor(blockPosition, this._schema.BlockSize, access);
                } else if(allocate) {
                    blockAccessor = this.AllocateBlock(out blockPosition);
                }
            } else {
                MemoryMappedViewAccessor nextBlockAccessor = null;
                long nextBlockPosition = blockAccessor.ReadInt64(this._schema.DataBlockSize);
                if(nextBlockPosition > 0) {
                    nextBlockAccessor = this._file.CreateViewAccessor(nextBlockPosition, this._schema.BlockSize, access);
                } else if(allocate) {
                    nextBlockAccessor = this.AllocateBlock(out nextBlockPosition);
                    blockAccessor.Write(this._schema.DataBlockSize, nextBlockPosition);
                } else {
                    return false;
                }
                blockAccessor.Dispose();
                blockAccessor = nextBlockAccessor;
                blockPosition = nextBlockPosition;
            }
            return true;
        }

        public InputStream CreateInputStream() {
            InputStream stream = InputStream.Create(this);
            if(stream != null) {
                this._schema.StreamCount++;
            }
            return stream;
        }

        public InputStream CreateInputStream(Token token) {
            return InputStream.Create(this, token);
        }

        public OutputStream CreateOutputStream(Token token) {
            return OutputStream.Create(this, token);
        }

        public bool DeleteStream(Token token) {
            if(token.HeadBlockPosition < 0) {
                return false;
            }
            Header header;
            using(MemoryMappedViewAccessor headBlockAccessor = this._file.CreateViewAccessor(token.HeadBlockPosition, this._schema.BlockSize)) {
                headBlockAccessor.Read<Header>(0, out header);
                if(!header.HashCode.Equals(token.HashCode)) {
                    return false;
                }
                Header nullHeader = default(Header);
                headBlockAccessor.Write<Header>(0, ref nullHeader);
            }
            using(MemoryMappedViewAccessor endBlockAccessor = this._file.CreateViewAccessor(header.EndBlockPosition, this._schema.BlockSize)) {
                endBlockAccessor.Write(this._schema.DataBlockSize, this._schema.HeadFreeBlockPosition);
            }
            this._schema.HeadFreeBlockPosition = token.HeadBlockPosition;
            this._schema.StreamCount--;
            return true;
        }

        public void Dispose() {
            using(MemoryMappedViewAccessor schemaAccessor = this._file.CreateViewAccessor(0, Schema.Size)) {
                schemaAccessor.Write<Schema>(0, ref this._schema);
            }
        }

        public class InputStream : Stream {

            private StreamBase _base;
            private MemoryMappedViewAccessor _blockAccessor;
            private int _blockOffset;
            private long _blockPosition;
            private long _headBlockPosition;
            private long _position;

            private InputStream() { }

            internal static InputStream Create(StreamBase @base) {
                InputStream stream = new InputStream();
                stream._base = @base;
                if(!stream._base.MoveToNextBlock(ref stream._headBlockPosition, ref stream._blockAccessor, MemoryMappedFileAccess.ReadWrite, true)) {
                    return null;
                }
                stream._blockOffset = stream._base._schema.StreamHeaderSize;
                stream._blockPosition = stream._headBlockPosition;
                stream._position = 0;
                return stream;
            }

            internal static InputStream Create(StreamBase @base, Token token) {
                InputStream stream = new InputStream();
                stream._base = @base;
                stream._headBlockPosition = token.HeadBlockPosition;
                if(!stream._base.MoveToNextBlock(ref stream._headBlockPosition, ref stream._blockAccessor, MemoryMappedFileAccess.ReadWrite)) {
                    return null;
                }
                Header header;
                stream._blockAccessor.Read<Header>(0, out header);
                if(!header.HashCode.Equals(token.HashCode)) {
                    return null;
                }
                stream._blockOffset = stream._base._schema.StreamHeaderSize;
                stream._blockPosition = stream._headBlockPosition;
                stream._position = 0;
                return stream;
            }

            public Token Token {
                get {
                    return new Token(this._headBlockPosition);
                }
            }

            public override bool CanRead {
                get { return false; }
            }

            public override bool CanSeek {
                get { return false; }
            }

            public override bool CanWrite {
                get { return true; }
            }

            public override void Flush() { }

            public override long Length {
                get { return this._position; }
            }

            public override long Position {
                get {
                    return this._position;
                }
                set {
                    throw new NotImplementedException();
                }
            }

            public override int Read(byte[] buffer, int offset, int count) {
                throw new NotImplementedException();
            }

            public override long Seek(long offset, SeekOrigin origin) {
                throw new NotImplementedException();
            }

            public override void SetLength(long value) {
                throw new NotImplementedException();
            }

            public override void Write(byte[] buffer, int offset, int count) {
                while(true) {
                    if(this._blockOffset == this._base._schema.DataBlockSize) {
                        for(int i = count / this._base._schema.DataBlockSize; i > 0; i--) {
                            this._base.MoveToNextBlock(ref this._blockPosition, ref this._blockAccessor, MemoryMappedFileAccess.ReadWrite, true);
                            this._blockAccessor.WriteArray<byte>(0, buffer, offset, this._base._schema.DataBlockSize);
                            offset += this._base._schema.DataBlockSize;
                        }
                        this._blockOffset = this._base._schema.DataBlockSize;
                        int remainingCount = count % this._base._schema.DataBlockSize;
                        if(remainingCount > 0) {
                            this._base.MoveToNextBlock(ref this._blockPosition, ref this._blockAccessor, MemoryMappedFileAccess.ReadWrite, true);
                            this._blockAccessor.WriteArray<byte>(0, buffer, offset, remainingCount);
                            this._blockOffset = remainingCount;
                        }
                        this._position += count;
                        return;
                    } else {
                        int blockWriteSize = this._base._schema.DataBlockSize - this._blockOffset;
                        if(count <= blockWriteSize) {
                            this._blockAccessor.WriteArray<byte>(this._blockOffset, buffer, offset, count);
                            this._blockOffset += count;
                            this._position += count;
                            return;
                        } else {
                            this._blockAccessor.WriteArray<byte>(this._blockOffset, buffer, offset, blockWriteSize);
                            this._blockOffset += blockWriteSize;
                            this._position += blockWriteSize;
                            offset += blockWriteSize;
                            count -= blockWriteSize;
                        }
                    }
                }
            }

            protected override void Dispose(bool disposing) {
                base.Dispose(disposing);
                if(disposing) {
                    using(MemoryMappedViewAccessor headBlockAccessor = this._base._file.CreateViewAccessor(this._headBlockPosition, this._base._schema.BlockSize)) {
                        Header header = new Header(this.Token, this._blockPosition, this._position);
                        headBlockAccessor.Write<Header>(0, ref header);
                    }
                    long nextBlockPosition = this._blockAccessor.ReadInt64(this._base._schema.DataBlockSize);
                    if(nextBlockPosition > 0) {
                        using(MemoryMappedViewAccessor nextBlockAccessor = this._base._file.CreateViewAccessor(nextBlockPosition, this._base._schema.BlockSize)) {
                            nextBlockAccessor.Write(this._base._schema.DataBlockSize, this._base._schema.HeadFreeBlockPosition);
                        }
                        this._base._schema.HeadFreeBlockPosition = nextBlockPosition;
                    }
                    this._blockAccessor.Write(this._base._schema.DataBlockSize, -1L);
                    this._blockAccessor.Dispose();
                    this._blockAccessor = null;
                    this._blockPosition = -1;
                    this._blockOffset = -1;
                    this._headBlockPosition = -1;
                    this._position = -1;
                    this._base = null;
                }
            }
        }

        public class OutputStream : Stream {

            private StreamBase _base;
            private MemoryMappedViewAccessor _blockAccessor;
            private int _blockOffset;
            private long _blockPosition;
            private long _headBlockPosition;
            private long _position;
            private long _length;

            private OutputStream() { }

            internal static OutputStream Create(StreamBase @base, Token token) {
                OutputStream stream = new OutputStream();
                stream._base = @base;
                stream._headBlockPosition = token.HeadBlockPosition;
                if(!stream._base.MoveToNextBlock(ref stream._headBlockPosition, ref stream._blockAccessor, MemoryMappedFileAccess.ReadWrite)) {
                    return null;
                }
                Header header;
                stream._blockAccessor.Read<Header>(0, out header);
                if(!header.HashCode.Equals(token.HashCode)) {
                    return null;
                }
                stream._blockOffset = stream._base._schema.StreamHeaderSize;
                stream._blockPosition = stream._headBlockPosition;
                stream._position = 0;
                stream._length = header.Length;
                return stream;
            }

            public Token Token {
                get {
                    return new Token(this._headBlockPosition);
                }
            }

            public override bool CanRead {
                get { return true; }
            }

            public override bool CanSeek {
                get { return false; }
            }

            public override bool CanWrite {
                get { return false; }
            }

            public override void Flush() { }

            public override long Length {
                get { return this._length; }
            }

            public override long Position {
                get {
                    return this._position;
                }
                set {
                    throw new NotImplementedException();
                }
            }

            public override long Seek(long offset, SeekOrigin origin) {
                throw new NotImplementedException();
            }

            public override void SetLength(long value) {
                throw new NotImplementedException();
            }

            public override void Write(byte[] buffer, int offset, int count) {
                throw new NotImplementedException();
            }

            public override int Read(byte[] buffer, int offset, int count) {
                if(this._position + count > this._length) {
                    count = (int)(this._length - this._position);
                }
                for(int _count = count; true; ) {
                    if(this._blockOffset == this._base._schema.DataBlockSize) {
                        for(int i = _count / this._base._schema.DataBlockSize; i > 0; i--) {
                            this._base.MoveToNextBlock(ref this._blockPosition, ref this._blockAccessor, MemoryMappedFileAccess.ReadWrite, true);
                            this._blockAccessor.ReadArray<byte>(0, buffer, offset, this._base._schema.DataBlockSize);
                            offset += this._base._schema.DataBlockSize;
                        }
                        this._blockOffset = this._base._schema.DataBlockSize;
                        int remainingCount = _count % this._base._schema.DataBlockSize;
                        if(remainingCount > 0) {
                            this._base.MoveToNextBlock(ref this._blockPosition, ref this._blockAccessor, MemoryMappedFileAccess.ReadWrite, true);
                            this._blockAccessor.ReadArray<byte>(0, buffer, offset, remainingCount);
                            this._blockOffset = remainingCount;
                        }
                        this._position += _count;
                        break;
                    } else {
                        int blockReadSize = this._base._schema.DataBlockSize - this._blockOffset;
                        if(_count <= blockReadSize) {
                            this._blockAccessor.ReadArray<byte>(this._blockOffset, buffer, offset, _count);
                            this._blockOffset += _count;
                            this._position += _count;
                            break;
                        } else {
                            this._blockAccessor.ReadArray<byte>(this._blockOffset, buffer, offset, blockReadSize);
                            this._blockOffset += blockReadSize;
                            this._position += blockReadSize;
                            offset += blockReadSize;
                            _count -= blockReadSize;
                        }
                    }
                }
                return count;
            }

            protected override void Dispose(bool disposing) {
                base.Dispose(disposing);
                if(disposing) {
                    this._blockAccessor.Dispose();
                    this._blockAccessor = null;
                    this._blockPosition = -1;
                    this._blockOffset = -1;
                    this._headBlockPosition = -1;
                    this._position = -1;
                    this._base = null;
                }
            }
        }
    }
}
